using Sustainalytics.ESGRatings.BusinessLogic.Computing.Exceptions;
using Sustainalytics.ESGRatings.BusinessLogic.Computing.Utils;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Sustainalytics.ESGRatings.BusinessLogic.Computing
{
    internal enum ComputationSetTypeEnum
    {
        PeerGroup,

        IndustryGroup,

        GovernanceSet
    }

    internal class ESGSetList
    {
        private static int _currentId;

        private readonly IEnumerable<Type> _comparativeGroupTypes = new List<Type>
                                                                        {
                                                                            typeof(IndustryGroupSet),
                                                                            typeof(PeerGroupSet),
                                                                            typeof(GovernanceSet)
                                                                        };

        private readonly object _lock = new object();

        private ConcurrentDictionary<GuidSetTypeTuple, IComputationSet> _esgSetList;

        public ESGSetList()
        {
            _esgSetList = new ConcurrentDictionary<GuidSetTypeTuple, IComputationSet>();
        }

        public bool Delete(Guid id)
        {
            if (id.Equals(Guid.Empty))
            {
                throw new UnknownSetException(id);
            }

            var result = true;

            foreach (var key in _esgSetList.Keys.Where(x => x.SetId == id))
            {
                IComputationSet set = null;
                result = result && _esgSetList.TryRemove(key, out set);
                if (set == null)
                {
                    throw new Exceptions.InvalidOperationException("Unable to delete set from list");
                }
            }

            return result;
        }

        public bool Delete(IEnumerable<Guid> setsIds)
        {
            lock (_lock)
            {
                if (setsIds != null)
                {
                    var setKeys = _esgSetList.Keys.Where(x => setsIds.Contains(x.SetId));
                    IComputationSet set;

                    return setKeys.Aggregate(true, (current, id) => current && _esgSetList.TryRemove(id, out set));
                }

                return false;
            }
        }

        internal void SetActive(Guid setId, bool isActive, Type comparativeGroupType)
        {
            lock (_lock)
            {
                if (setId == Guid.Empty)
                {
                    throw new UnknownSetException(setId);
                }

                if (_esgSetList.IsNullOrEmpty())
                {
                    throw new EmptySetListException();
                }

                IComputationSet set;
                if (_esgSetList.TryGetValue(new GuidSetTypeTuple(setId, comparativeGroupType), out set))
                    set.IsActive = isActive;
                _esgSetList.TryUpdate(new GuidSetTypeTuple(setId, comparativeGroupType), set, set);
            }
        }

        public bool GetSetByGuid(Guid id, Type comparativeGroupType, out IComputationSet set)
        {
            if (id == Guid.Empty)
            {
                throw new UnknownSetException(id);
            }

            if (_esgSetList.IsNullOrEmpty())
            {
                throw new EmptySetListException();
            }


            var result = _esgSetList.TryGetValue(new GuidSetTypeTuple(id, comparativeGroupType), out set);
            return result;
        }

        public bool GetSetByGuid(Guid id, out ICollection<IComputationSet> sets)
        {
            if (id == Guid.Empty)
            {
                throw new UnknownSetException(id);
            }

            var setTypes = GetGuidSetTypeTuples(new[] { id });
            sets = new List<IComputationSet>();
            var result = false;
            foreach (var set in setTypes.Select(
                        type => _esgSetList.Where(x => x.Key.Equals(type)).Select(x => x.Value).FirstOrDefault())
                        .Where(set => set != null))
            {
                result = true;
                sets.Add(set);
            }

            return result;
        }

        public IComputationSet GetSetByIdOrDefault(Guid? id, Type comparativeGroupType)
        {
            if (id.Equals(Guid.Empty) || !id.HasValue)
            {
                return _esgSetList.First(x => x.Value.IsActive == true).Value;
            }

            IComputationSet set;
            _esgSetList.TryGetValue(new GuidSetTypeTuple(id.Value, comparativeGroupType), out set);

            return set ?? _esgSetList.First().Value;
        }

        /// <exception cref="UnknownSetException">The set is not found in list.</exception>
        public ICollection<IComputationSet> GetSetByIdOrDefault(Guid? id)
        {
            if (id.Equals(Guid.Empty) || !id.HasValue)
            {
                var result = _esgSetList.First(x => x.Value.IsActive);
                if (result.Equals(default(KeyValuePair<GuidSetTypeTuple, IComputationSet>)))
                {
                    throw new UnknownSetException(id.GetValueOrDefault());
                }

                return new[] { result.Value };

                // setId = result.Key.SetId;
            }

            var setId = id.Value;

            ICollection<IComputationSet> sets;
            GetSetByGuid(setId, out sets);
            return sets;
        }

        public KeyValuePair<Guid, ESGSet> GetSetByMatrixAndReferenceUniverse(Guid matrixId, Guid referenceUniverseId, Type computationSetTypeEnum)
        {

            if (referenceUniverseId.Equals(Guid.Empty))
            {
                throw new UnknownUniverseException(referenceUniverseId);
            }

            Func<ESGSet, bool> predicate = set => set.GetLoadedUniverses().Contains(referenceUniverseId) && set.GetLoadedMatrices().Contains(matrixId);

            var esgSets = _esgSetList.Where(x => x.Key.ComparativeGroupType == computationSetTypeEnum && (x.Value is ESGSet)).OrderByDescending(x => x.Value.IsActive);

            foreach (var set in esgSets)
            {
                var esgSet = set.Value as ESGSet;

                if (esgSet != null && predicate(esgSet))
                {
                    return new KeyValuePair<Guid, ESGSet>(set.Key.SetId, esgSet);
                }
            }

            return new KeyValuePair<Guid, ESGSet>();
        }

        public IEnumerable<Guid> GetSetId(int id)
        {
            var result = _esgSetList.Where(x => x.Value.Id == id);
            return result.Select(x => x.Key.SetId).Distinct();
        }

        public IEnumerable<GuidSetTypeTuple> GetSetIds()
        {
            return _esgSetList.Keys;
        }

        public ICollection<T> GetSetList<T>()
        {
            return _esgSetList.Values.OfType<T>().ToList();
        }

        public bool IsNullOrEmpty()
        {
            lock (_lock)
            {
                return _esgSetList.IsNullOrEmpty();
            }
        }

        public int NextId()
        {
            if (_esgSetList.IsNullOrEmpty())
            {
                _currentId = 0;
            }
            else
            {
                _currentId += 1;
            }

            return _currentId;
        }

        public bool TryAdd(Guid id, IComputationSet set)
        {
            if (id.Equals(Guid.Empty))
            {
                throw new InvalidSetException();
            }

            if (_esgSetList == null)
            {
                _esgSetList = new ConcurrentDictionary<GuidSetTypeTuple, IComputationSet>();
            }

            var type = set.GetType();
            if (!_comparativeGroupTypes.Contains(type))
            {
                throw new UnknownSetException(string.Format("No set with type {0}", type));
            }

            if (set.Id < 0)
            {
                throw new InvalidSetException("Invalid internal set id");
            }

            IComputationSet sets;

            if (!_esgSetList.IsNullOrEmpty())
            {
                if (GetSetByGuid(id, set.GetType(), out sets))
                {
                    throw new DuplicateEntityException(id, set.GetType());
                }
            }

            return _esgSetList.TryAdd(new GuidSetTypeTuple(id, set.GetType()), set);
        }

        private IEnumerable<GuidSetTypeTuple> GetGuidSetTypeTuples(IEnumerable<Guid> setsIds)
        {
            IEnumerable<GuidSetTypeTuple> sets = from id in setsIds
                                                 from type in _comparativeGroupTypes
                                                 select new GuidSetTypeTuple(id, type);
            return sets;
        }
    }
}